[AWS CDK × Amazon ECS] ContainerImage.fromAsset でソースコードからビルドしたコンテナイメージを ApplicationLoadBalancedFargateService で使ってみた
こんにちは、製造ビジネステクノロジー部の若槻です。
AWS CDK の L3 Construct である aws_ecs_patterns module を使用すると、Amazon ECS アプリケーションの実装パターンを CDK で簡潔に実装することができます。
aws_ecs_patterns module では様々なパターンに対応したクラスが提供されており、例えば ALB + ECS on Fargate の場合は ApplicationLoadBalancedFargateService Construct クラスを、NLB + ECS on EC2 の場合は NetworkLoadBalancedEc2Service Construct クラスを使用して CDK の実装を抽象化することができます。
今回は、ContainerImage.fromAsset でソースコードからビルドしたコンテナイメージを ApplicationLoadBalancedFargateService で使用する CDK の実装を試してみました。
試してみた
コンテナアプリケーションの実装
ソースコード
const http = require('http');
const server = http.createServer((req, res) => {
const currentTime = new Date().toISOString();
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(`Current time: ${currentTime}`);
});
const port = 3000;
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Dockerfile
FROM node:20
WORKDIR /app
COPY index.js .
CMD ["node", "index.js"]
ローカルでの動作確認
$ docker run -p 3000:3000 my-nodejs-app
Server running at http://localhost:3000/
$ curl http://localhost:3000
Current time: 2024-09-01T08:42:58.400Z
CDK アプリケーションの実装
ContainerImage.fromAsset で先程のソースコードからビルドしたコンテナイメージを、ApplicationLoadBalancedFargateService クラスで使用して ECS サービスを作成します。
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns';
import { Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class CdkSampleStack extends Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// VPCの作成
const vpc = new ec2.Vpc(this, 'MyVpc');
// ECSクラスターの作成
const cluster = new ecs.Cluster(this, 'MyCluster', {
vpc,
});
// ソースコードからコンテナイメージをビルド
const image = ecs.ContainerImage.fromAsset('./nodejs-app');
// ApplicationLoadBalancedFargateService による ECS サービスの作成
new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'MyService', {
cluster,
taskImageOptions: {
image,
containerPort: 3000,
entryPoint: [],
command: ['node', 'index.js'],
},
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.ARM64,
},
});
}
}
上記 CDK アプリケーションをデプロイします。デプロイ中に Docker build および push が実行されていることが出力から確認できます。
$ npm run deploy
> cdk_sample_app@0.1.0 deploy
> cdk deploy --require-approval never --method=direct
✨ Synthesis time: 3.88s
CdkSampleStack: start: Building 2f71add72fca7642dbd03645d344c7254298c425fb86aeb8dbb449797ea94a12:current_account-current_region
CdkSampleStack: success: Built 2f71add72fca7642dbd03645d344c7254298c425fb86aeb8dbb449797ea94a12:current_account-current_region
CdkSampleStack: start: Publishing 2f71add72fca7642dbd03645d344c7254298c425fb86aeb8dbb449797ea94a12:current_account-current_region
CdkSampleStack: start: Building 1ac47cda849cffe5a100d6da66820c2ba24f174e0c3e3d374329766b76b0887d:current_account-current_region
CdkSampleStack: success: Published 2f71add72fca7642dbd03645d344c7254298c425fb86aeb8dbb449797ea94a12:current_account-current_region
#0 building with "rancher-desktop" instance using docker driver
#1 [internal] load .dockerignore
#1 transferring context: 2B done
#1 DONE 0.0s
#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 107B done
#2 DONE 0.0s
#3 [internal] load metadata for docker.io/library/node:20
#3 DONE 6.4s
#4 [1/3] FROM docker.io/library/node:20@sha256:a4d1de4c7339eabcf78a90137dfd551b798829e3ef3e399e0036ac454afa1291
#4 DONE 0.0s
#5 [internal] load build context
#5 transferring context: 332B done
#5 DONE 0.0s
#6 [2/3] WORKDIR /app
#6 CACHED
#7 [3/3] COPY index.js .
#7 DONE 0.0s
#8 exporting to image
#8 exporting layers 0.0s done
#8 writing image sha256:8a9154b19dd2e50970f6e563be6e402e809f1885c15ae6b6b9934f60166e7a99 done
#8 naming to docker.io/library/cdkasset-1ac47cda849cffe5a100d6da66820c2ba24f174e0c3e3d374329766b76b0887d done
#8 DONE 0.0s
CdkSampleStack: success: Built 1ac47cda849cffe5a100d6da66820c2ba24f174e0c3e3d374329766b76b0887d:current_account-current_region
CdkSampleStack: start: Publishing 1ac47cda849cffe5a100d6da66820c2ba24f174e0c3e3d374329766b76b0887d:current_account-current_region
The push refers to repository [XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/cdk-hnb659fds-container-assets-XXXXXXXXXXXX-ap-northeast-1]
b9d7ed151fdb: Preparing
354bed3f83bf: Preparing
4ba5faeca2b1: Preparing
b9fd3ed7c25e: Preparing
57202e75e02e: Preparing
11debb8d6358: Preparing
f752cb05a39e: Preparing
20f026ae0a91: Preparing
f21c087a3964: Preparing
cedb364ef937: Preparing
11debb8d6358: Waiting
f752cb05a39e: Waiting
20f026ae0a91: Waiting
f21c087a3964: Waiting
cedb364ef937: Waiting
b9fd3ed7c25e: Layer already exists
4ba5faeca2b1: Layer already exists
354bed3f83bf: Layer already exists
57202e75e02e: Layer already exists
20f026ae0a91: Layer already exists
f21c087a3964: Layer already exists
f752cb05a39e: Layer already exists
11debb8d6358: Layer already exists
cedb364ef937: Layer already exists
b9d7ed151fdb: Pushed
1ac47cda849cffe5a100d6da66820c2ba24f174e0c3e3d374329766b76b0887d: digest: sha256:7b6aedf28f477afa5fd8e3b5c5fa099169654bfefd6f775ceef351bd77ddce4d size: 2417
CdkSampleStack: success: Published 1ac47cda849cffe5a100d6da66820c2ba24f174e0c3e3d374329766b76b0887d:current_account-current_region
CdkSampleStack: deploying... [1/1]
CdkSampleStack: updating stack...
✅ CdkSampleStack
✨ Deployment time: 219.88s
Outputs:
CdkSampleStack.MyServiceLoadBalancerDNS1782DE5A = CdkSam-MySer-uSkIJ3rVyqoB-170625474.ap-northeast-1.elb.amazonaws.com
CdkSampleStack.MyServiceServiceURL4C379FE3 = http://CdkSam-MySer-uSkIJ3rVyqoB-170625474.ap-northeast-1.elb.amazonaws.com
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/fd7023d0-653d-11ef-b8c3-0e28b5cf9e67
✨ Total time: 223.76s
デプロイ完了後に CloudFormation のダッシュボードで Construct ツリーを確認すると、ApplicationLoadBalancedFargateService Construct により ALB、ECS タスクおよびサービスが作成されていることが確認できます。
またイメージはアカウント/リージョン毎に共通の ECR レジストリにプッシュされ、自動的にバージョニングが行われます。
この共通の ECR レジストリの利用に関してセキュリティやデプロイ戦略などの都合で受け入れられない場合は ContainerImage.fromRegistry クラスでレジストリを指定する方式も選択肢としてあります。
そして作成された ECS サービスのエンドポイント(ALB の DNS 名)にアクセスすると、期待したレスポンスが返ってくるのでコンテナアプリケーションが動作していることが確認できました。
$ curl http://cdksam-myser-uskij3rvyqob-170625474.ap-northeast-1.elb.amazonaws.com/
Current time: 2024-09-01T09:35:09.855Z
アプリケーション実装を更新する
アプリケーション実装を更新した場合は差分が適切にデプロイされるのでしょうか。
実装に差分が無い場合は、デプロイをキックしてももちろん変更無しとして扱われます。
$ npm run deploy
> cdk_sample_app@0.1.0 deploy
> cdk deploy --require-approval never --method=direct
✨ Synthesis time: 4.77s
CdkSampleStack: deploying... [1/1]
✅ CdkSampleStack (no changes)
✨ Deployment time: 1.04s
Outputs:
CdkSampleStack.MyServiceLoadBalancerDNS1782DE5A = CdkSam-MySer-uSkIJ3rVyqoB-170625474.ap-northeast-1.elb.amazonaws.com
CdkSampleStack.MyServiceServiceURL4C379FE3 = http://CdkSam-MySer-uSkIJ3rVyqoB-170625474.ap-northeast-1.elb.amazonaws.com
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/CdkSampleStack/fd7023d0-653d-11ef-b8c3-0e28b5cf9e67
✨ Total time: 5.81s
アプリケーション実装に差分を追加してみます。
$ git diff
diff --git a/nodejs-app/index.js b/nodejs-app/index.js
index a60833a..bcd86c4 100644
--- a/nodejs-app/index.js
+++ b/nodejs-app/index.js
@@ -1,10 +1,9 @@
const http = require('http');
const server = http.createServer((req, res) => {
- const currentTime = new Date().toISOString();
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
- res.end(`Current time: ${currentTime}`);
+ res.end('Hello, CDK!');
});
const port = 3000;
CDK Diff で CDK 実装の差分を確認すると、コンテナイメージの差分が検出されていることが確認できます。
$ npx cdk diff
Stack CdkSampleStack
Hold on while we create a read-only change set to get a diff with accurate replacement information (use --no-change-set to use a less accurate but faster template-only diff)
Resources
[~] AWS::ECS::TaskDefinition MyService/TaskDef MyServiceTaskDefB25792F0 replace
└─ [~] ContainerDefinitions (requires replacement)
└─ @@ -7,7 +7,7 @@
[ ] "EntryPoint": [],
[ ] "Essential": true,
[ ] "Image": {
[-] "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:1b2e6eafe309af2dec3c8a044ff8e0e28b093d369cf162eb58875a0f8f1cd23a"
[+] "Fn::Sub": "${AWS::AccountId}.dkr.ecr.${AWS::Region}.${AWS::URLSuffix}/cdk-hnb659fds-container-assets-${AWS::AccountId}-${AWS::Region}:1ac47cda849cffe5a100d6da66820c2ba24f174e0c3e3d374329766b76b0887d"
[ ] },
[ ] "LogConfiguration": {
[ ] "LogDriver": "awslogs",
✨ Number of stacks with differences: 1
CDK デプロイしてエンドポイントにアクセスすると、アプリケーション実装の変更が反映されていることが確認できました。
$ curl http://CdkSam-MySer-uSkIJ3rVyqoB-170625474.ap-northeast-1.elb.amazonaws.com
Hello, CDK!
このように実装に差分がある場合と無い場合で同じコマンドでデプロイでき、冪等性が保たれているのは便利ですね。
トラブルシュート
デプロイ時にタスクで exec format error が発生する
CDK デプロイ時に ECS タスクで次のログが出力されてデプロイが失敗しました。
exec /usr/local/bin/docker-entrypoint.sh: exec format error
runtimePlatform.cpuArchitecture で適切な CPU アーキテクチャを指定する必要があったためでした。今回はデプロイ元は M1 Mac であるためコンテナイメージが ARM64 用にビルドされている必要がありました。
デプロイ時にタスクで SyntaxError: Cannot use import statement outside a module が発生する
CDK デプロイ時に ECS タスクで次のログが出力されてデプロイが失敗しました。
/app/index.js:1
import http from 'http';
^^^^^^
SyntaxError: Cannot use import statement outside a module
これはエラーの通りで、コンテナの環境で ES モジュールをサポートしていないためでした。そこでソースコード側で import 文を require 文に変更する対応をしました。
$ git diff
diff --git a/nodejs-app/index.js b/nodejs-app/index.js
index 8723110..bcd86c4 100644
--- a/nodejs-app/index.js
+++ b/nodejs-app/index.js
@@ -1,4 +1,4 @@
-import * as http from 'http';
+const http = require('http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
おわりに
ContainerImage.fromAsset でソースコードからビルドしたコンテナイメージを ApplicationLoadBalancedFargateService で使用する CDK の実装を試してみました。
ALB の作成からコンテナのプッシュまで高度に抽象化がされつつ、Load Balancer や VPC は先に作成したものを使用できるなどある程度はカスタマイズは可能となっています。使い所を選べば非常に便利なモジュールなので、ぜひ活用してみてください。
以上